// @ts-nocheck import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common' import Head from 'next/head' import Link from 'next/link' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { AWS_REGIONS, type CloudProvider } from 'shared-data' import { toast } from 'sonner' import { Button, Form, useWatch } from 'ui' import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { z } from 'zod' import { AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL } from '@/components/interfaces/Database/Triggers/EventTriggersList/EventTriggers.constants' import { AdvancedConfiguration } from '@/components/interfaces/ProjectCreation/AdvancedConfiguration' import { ComputeSizeSelector } from '@/components/interfaces/ProjectCreation/ComputeSizeSelector' import { DatabasePasswordInput } from '@/components/interfaces/ProjectCreation/DatabasePasswordInput' import { DisabledWarningDueToIncident } from '@/components/interfaces/ProjectCreation/DisabledWarningDueToIncident' import { FreeProjectLimitWarning } from '@/components/interfaces/ProjectCreation/FreeProjectLimitWarning' import { InternalOnlyConfiguration } from '@/components/interfaces/ProjectCreation/InternalOnlyConfiguration' import { OrganizationSelector } from '@/components/interfaces/ProjectCreation/OrganizationSelector' import { extractPostgresVersionDetails } from '@/components/interfaces/ProjectCreation/PostgresVersionSelector' import { sizes } from '@/components/interfaces/ProjectCreation/ProjectCreation.constants' import { FormSchema } from '@/components/interfaces/ProjectCreation/ProjectCreation.schema' import { instanceLabel, smartRegionToExactRegion, } from '@/components/interfaces/ProjectCreation/ProjectCreation.utils' import { ProjectCreationFooter } from '@/components/interfaces/ProjectCreation/ProjectCreationFooter' import { ProjectNameInput } from '@/components/interfaces/ProjectCreation/ProjectNameInput' import { RegionSelector } from '@/components/interfaces/ProjectCreation/RegionSelector' import { SecurityOptions } from '@/components/interfaces/ProjectCreation/SecurityOptions' import { GitHubRepositoryField, useGitHubRepositoryOptions, } from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField' import DefaultLayout from '@/components/layouts/DefaultLayout' import { WizardLayoutWithoutAuth } from '@/components/layouts/WizardLayout' import Panel from '@/components/ui/Panel' import { useAvailableOrioleImageVersion } from '@/data/config/project-creation-postgres-versions-query' import { useOverdueInvoicesQuery } from '@/data/invoices/invoices-overdue-query' import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query' import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query' import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query' import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { DesiredInstanceSize, instanceSizeSpecs } from '@/data/projects/new-project.constants' import { OrgProject, useOrgProjectsInfiniteQuery, } from '@/data/projects/org-projects-infinite-query' import { ProjectCreateVariables, useProjectCreateMutation, } from '@/data/projects/project-create-mutation' import { useCustomContent } from '@/hooks/custom-content/useCustomContent' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useDataApiRevokeOnCreateDefaultEnabled } from '@/hooks/misc/useDataApiRevokeOnCreateDefault' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { withAuth } from '@/hooks/misc/withAuth' import { usePHFlag } from '@/hooks/ui/useFlag' import { DOCS_URL, PROJECT_STATUS, PROVIDERS, useDefaultProvider } from '@/lib/constants' import { buildStudioPageTitle } from '@/lib/page-title' import { useProfile } from '@/lib/profile' import { useTrack } from '@/lib/telemetry/track' import type { NextPageWithLayout } from '@/types' const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small'] const Wizard: NextPageWithLayout = () => { const track = useTrack() const router = useRouter() const { slug, projectName } = useParams() const { appTitle } = useCustomContent(['app:title']) const defaultProvider = useDefaultProvider() const { profile } = useProfile() const pageTitle = buildStudioPageTitle({ section: 'New Project', brand: appTitle || 'Briven', }) const { data: currentOrg } = useSelectedOrganizationQuery() const isFreePlan = currentOrg?.plan?.id === 'free' const canChooseInstanceSize = !isFreePlan const [lastVisitedOrganization] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, '' ) const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects') const { can: canCreateGitHubConnection } = useAsyncCheckPermissions( PermissionAction.CREATE, 'integrations.github_connections' ) const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config') const { hasAccess: hasAccessToGitHubIntegration } = useCheckEntitlements( 'integrations.github_connections' ) const smartRegionEnabled = useFlag('enableSmartRegion') const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate') const showInternalOnlyConfiguration = useFlag('newProjectInternalOnlyConfiguration') // Read the raw flag for telemetry — coerce-undefined-to-false would record false for // users whose flags haven't loaded yet. The raw value preserves undefined (omitted from // PostHog) so we only record true/false when the flag is resolved. const dataApiRevokeOnCreateDefaultFlag = usePHFlag('dataApiRevokeOnCreateDefault') const isDataApiRevokeOnCreateDefault = useDataApiRevokeOnCreateDefaultEnabled() const isNotOnHigherPlan = !['team', 'enterprise', 'platform'].includes(currentOrg?.plan.id ?? '') // This is to make the database.new redirect work correctly. The database.new redirect should be set to supabase.com/dashboard/new/last-visited-org if (slug === 'last-visited-org') { if (lastVisitedOrganization) { router.replace(`/new/${lastVisitedOrganization}`, undefined, { shallow: true }) } else { router.replace(`/new/_`, undefined, { shallow: true }) } } const [allProjects, setAllProjects] = useState(undefined) const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] = useState(false) const form = useForm>({ resolver: zodResolver(FormSchema as any), mode: 'onChange', defaultValues: { organization: slug, projectName: projectName || '', highAvailability: false, postgresVersion: '', instanceType: '', cloudProvider: PROVIDERS[defaultProvider].id, dbPass: '', dbPassStrength: 0, dbPassStrengthMessage: '', dbRegion: undefined, githubRepositoryId: '', githubInstallationId: undefined, githubRepositoryName: '', instanceSize: canChooseInstanceSize ? sizes[0] : undefined, dataApi: true, dataApiDefaultPrivileges: !isDataApiRevokeOnCreateDefault, enableRlsEventTrigger: false, postgresVersionSelection: '', useOrioleDb: false, }, }) const { getFieldState, resetField, setValue } = form const { instanceSize: watchedInstanceSize, cloudProvider, dbRegion, githubRepositoryName, organization, projectName: watchedProjectName, highAvailability, } = useWatch({ control: form.control }) // Read dirty state during render rather than depending on form.formState in the // effect — form.formState is a Proxy that gets a new reference every render, which // would re-fire this effect after each setValue and trigger an infinite loop. const isDataApiDefaultPrivilegesDirty = getFieldState( 'dataApiDefaultPrivileges', form.formState ).isDirty useEffect(() => { if (dataApiRevokeOnCreateDefaultFlag === undefined) return if (isDataApiDefaultPrivilegesDirty) return setValue('dataApiDefaultPrivileges', !dataApiRevokeOnCreateDefaultFlag, { shouldDirty: false, }) }, [dataApiRevokeOnCreateDefaultFlag, isDataApiDefaultPrivilegesDirty, setValue]) // [Charis] Since the form is updated in a useEffect, there is an edge case // when switching from free to paid, where canChooseInstanceSize is true for // an in-between render, but watchedInstanceSize is still undefined from the // form state carried over from the free plan. To avoid this, we set a // default instance size in this case. const instanceSize = canChooseInstanceSize ? (watchedInstanceSize ?? sizes[0]) : undefined const { data: membersExceededLimit = [] } = useFreeProjectLimitCheckQuery( { slug }, { enabled: isFreePlan } ) const hasMembersExceedingFreeTierLimit = membersExceededLimit.length > 0 const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit const { data: organizations = [], isSuccess: isOrganizationsSuccess } = useOrganizationsQuery() const isEmptyOrganizations = isOrganizationsSuccess && organizations.length <= 0 const { data: approvedOAuthApps = [] } = useAuthorizedAppsQuery( { slug }, { enabled: !isFreePlan && slug !== '_' } ) const hasOAuthApps = approvedOAuthApps.length > 0 const { data: allOverdueInvoices = [] } = useOverdueInvoicesQuery({ enabled: isNotOnHigherPlan, }) const overdueInvoices = allOverdueInvoices.filter((x) => x.organization_id === currentOrg?.id) const hasOutstandingInvoices = isNotOnHigherPlan && overdueInvoices.length > 0 const { data: orgProjectsFromApi } = useOrgProjectsInfiniteQuery({ slug: currentOrg?.slug }) const allOrgProjects = useMemo( () => orgProjectsFromApi?.pages.flatMap((page) => page.projects), [orgProjectsFromApi?.pages] ) const organizationProjects = allProjects?.filter((project) => project.status !== PROJECT_STATUS.INACTIVE) ?? [] const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0 const additionalMonthlySpend = isFreePlan ? 0 : instanceSizeSpecs[instanceSize as DesiredInstanceSize]!.priceMonthly - availableComputeCredits const { data: _defaultRegion, error: defaultRegionError } = useDefaultRegionQuery( { cloudProvider: PROVIDERS[defaultProvider].id, }, { enabled: !smartRegionEnabled, refetchOnMount: false, refetchOnWindowFocus: false, refetchInterval: false, refetchOnReconnect: false, retry: false, } ) const { data: availableRegionsData, error: availableRegionsError } = useOrganizationAvailableRegionsQuery( { slug: slug, cloudProvider: PROVIDERS[cloudProvider as CloudProvider].id, desiredInstanceSize: instanceSize as DesiredInstanceSize, }, { enabled: smartRegionEnabled, refetchOnMount: false, refetchOnWindowFocus: false, refetchInterval: false, refetchOnReconnect: false, } ) const recommendedSmartRegion = smartRegionEnabled ? availableRegionsData?.recommendations.smartGroup.name : '' const regionError = smartRegionEnabled && defaultProvider !== 'AWS_NIMBUS' ? availableRegionsError : defaultRegionError const defaultRegion = defaultProvider === 'AWS_NIMBUS' ? AWS_REGIONS.EAST_US.displayName : smartRegionEnabled ? availableRegionsData?.recommendations.smartGroup.name : _defaultRegion const canCreateProject = isAdmin && !freePlanWithExceedingLimits && !hasOutstandingInvoices const canConfigureGitHubOnCreate = canCreateProject && hasAccessToGitHubIntegration && canCreateGitHubConnection const dbRegionExact = smartRegionToExactRegion(dbRegion ?? '') const availableOrioleVersion = useAvailableOrioleImageVersion( { cloudProvider: cloudProvider as CloudProvider, dbRegion: smartRegionEnabled ? dbRegionExact : (dbRegion ?? ''), organizationSlug: organization, }, { enabled: currentOrg !== null } ) const userPrimaryEmail = profile?.primary_email?.toLowerCase() const isUserAtFreeProjectLimit = userPrimaryEmail ? membersExceededLimit.some( (member) => member.primary_email?.toLowerCase() === userPrimaryEmail ) : false const shouldShowFreeProjectInfo = !!currentOrg && !isFreePlan && !isUserAtFreeProjectLimit const { gitHubAuthorization, githubRepos, hasPartialResponseDueToSSO, isLoading: isLoadingRepositoryOptions, refetch: refetchRepositoryOptions, } = useGitHubRepositoryOptions() const { mutate: createProject, isPending: isCreatingNewProject, isSuccess: isSuccessNewProject, } = useProjectCreateMutation({ onSuccess: (res) => { track( 'project_creation_simple_version_submitted', { surface: 'main', instanceSize: form.getValues('instanceSize'), enableRlsEventTrigger: form.getValues('enableRlsEventTrigger'), dataApiEnabled: form.getValues('dataApi'), dataApiDefaultPrivilegesGranted: form.getValues('dataApiDefaultPrivileges'), useOrioleDb: form.getValues('useOrioleDb'), ...(dataApiRevokeOnCreateDefaultFlag !== undefined && { dataApiRevokeOnCreateDefaultEnabled: dataApiRevokeOnCreateDefaultFlag, }), }, { project: res.ref, organization: res.organization_slug, } ) router.push(`/project/${res.ref}`) }, }) const onSubmitWithComputeCostsConfirmation = async (values: z.infer) => { const launchingLargerInstance = values.instanceSize && !sizesWithNoCostConfirmationRequired.includes(values.instanceSize as DesiredInstanceSize) if (additionalMonthlySpend > 0 && (hasOAuthApps || launchingLargerInstance)) { track('project_creation_simple_version_confirm_modal_opened', { instanceSize: values.instanceSize, }) setIsComputeCostsConfirmationModalVisible(true) } else { await onSubmit(values) } } const onSubmit = async (values: z.infer) => { if (!currentOrg) return console.error('Unable to retrieve current organization') const { cloudProvider, projectName, highAvailability, dbPass, dbRegion, postgresVersion, instanceType, instanceSize, dataApi, dataApiDefaultPrivileges, enableRlsEventTrigger, postgresVersionSelection, useOrioleDb, githubInstallationId, githubRepositoryId, } = values if (useOrioleDb && !availableOrioleVersion) { return toast.error('No available OrioleDB image found, only Postgres is available') } const { postgresEngine, releaseChannel } = extractPostgresVersionDetails(postgresVersionSelection) const { smartGroup = [], specific = [] } = availableRegionsData?.all ?? {} const selectedRegion = smartRegionEnabled ? (smartGroup.find((x) => x.name === dbRegion) ?? specific.find((x) => x.name === dbRegion)) : undefined const parsedGitHubRepositoryId = githubRepositoryId.length > 0 ? Number(githubRepositoryId) : undefined const shouldIncludeGitHubFields = githubInstallationId !== undefined && Number.isFinite(parsedGitHubRepositoryId) const data: ProjectCreateVariables = { dbPass, cloudProvider, organizationSlug: currentOrg.slug, name: projectName, highAvailability, // gets ignored due to org billing subscription anyway dbPricingTierId: 'tier_free', // only set the compute size on pro+ plans. Free plans always use micro (nano in the future) size. dbInstanceSize: isFreePlan ? undefined : (instanceSize as DesiredInstanceSize), dataApiExposedSchemas: !dataApi ? [] : undefined, dataApiUseApiSchema: false, dataApiRevokeDefaultPrivileges: dataApi && !dataApiDefaultPrivileges, postgresEngine: useOrioleDb ? availableOrioleVersion?.postgres_engine : postgresEngine, releaseChannel: useOrioleDb ? availableOrioleVersion?.release_channel : releaseChannel, ...(smartRegionEnabled ? { regionSelection: selectedRegion } : { dbRegion }), dbSql: enableRlsEventTrigger ? AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL : undefined, ...(shouldIncludeGitHubFields ? { githubInstallationId, githubRepositoryId: parsedGitHubRepositoryId, } : {}), } if (postgresVersion && !postgresVersion.match(/1[2-9]\..*/)) { toast.error( `Invalid Postgres version, should start with a number between 12-19, a dot and additional characters, i.e. 15.2 or 15.2.0-3` ) } if (postgresVersion || instanceType) { data['customBrivenRequest'] = { ami: { ...(postgresVersion && { search_tags: { 'tag:postgresVersion': postgresVersion }, }), ...(instanceType && { instance_type: instanceType }), }, } } createProject(data) } useEffect(() => { // Only set once to ensure compute credits dont change while project is being created if (allOrgProjects && allOrgProjects.length > 0 && !allProjects) { setAllProjects(allOrgProjects) } }, [allOrgProjects, allProjects, setAllProjects]) useEffect(() => { // Handle no org: redirect to new org route if (isEmptyOrganizations) { router.push(`/new`) } }, [isEmptyOrganizations, router]) useEffect(() => { // [Joshen] Cause slug depends on router which doesnt load immediately on render // While the form data does load immediately if (slug && slug !== '_') setValue('organization', slug) if (projectName) setValue('projectName', projectName || '') }, [slug, setValue, projectName]) const isDbRegionDirty = getFieldState('dbRegion', form.formState).isDirty useEffect(() => { if (!isDbRegionDirty && defaultRegion) { setValue('dbRegion', defaultRegion) } }, [defaultRegion, isDbRegionDirty, setValue]) useEffect(() => { if (regionError) { resetField('dbRegion', { defaultValue: PROVIDERS[defaultProvider].default_region.displayName, }) } }, [regionError, resetField, defaultProvider]) useEffect(() => { if (!isDbRegionDirty && recommendedSmartRegion) { setValue('dbRegion', recommendedSmartRegion) } }, [recommendedSmartRegion, isDbRegionDirty, setValue]) useEffect(() => { if (highAvailability && cloudProvider !== 'AWS_K8S') { setValue('cloudProvider', 'AWS_K8S') } }, [highAvailability, cloudProvider, setValue]) useEffect(() => { if (watchedInstanceSize !== instanceSize) { setValue('instanceSize', instanceSize, { shouldDirty: false, shouldValidate: false, shouldTouch: false, }) } }, [instanceSize, watchedInstanceSize, setValue]) useEffect(() => { if (!githubRepositoryName) return if ((watchedProjectName ?? '').trim().length > 0) return const repoName = githubRepositoryName.split('/').at(-1) ?? githubRepositoryName setValue('projectName', repoName.trim(), { shouldValidate: true, }) }, [githubRepositoryName, watchedProjectName, setValue]) return ( <> {/* Wizard layouts set the visual header but not the browser tab title. */} {pageTitle}

Create a new project

Your project will have its own dedicated instance and full Postgres database. An API will be set up so you can easily interact with your new database.

} footer={ } > <> {projectCreationDisabled ? ( ) : (
{canCreateProject && ( <> {canConfigureGitHubOnCreate && ( Ideal for agent-first workflows: update your schema in code, push it to GitHub, and Briven deploys the changes automatically.{' '} Learn more } disabled={isCreatingNewProject} repositories={githubRepos} gitHubAuthorization={gitHubAuthorization} hasPartialResponseDueToSSO={hasPartialResponseDueToSSO} isLoading={isLoadingRepositoryOptions} refetch={refetchRepositoryOptions} onConnectClick={() => track('project_creation_github_connect_clicked')} /> )} {canChooseInstanceSize && } {showInternalOnlyConfiguration && } {showAdvancedConfig && !!availableOrioleVersion && ( )} {shouldShowFreeProjectInfo ? ( You can have up to 2 free projects across all organizations.{' '} Create a free organization {' '} to use them.

} /> ) : null} )} {freePlanWithExceedingLimits ? ( isAdmin && slug && ( ) ) : hasOutstandingInvoices ? (

Please resolve all outstanding invoices first before creating a new project

} /> ) : null} )}
setIsComputeCostsConfirmationModalVisible(false)} onConfirm={async () => { const values = form.getValues() await onSubmit(values) setIsComputeCostsConfirmationModalVisible(false) }} variant={'warning'} >

Launching a project on compute size "{instanceLabel(instanceSize)}" increases your monthly costs by ${additionalMonthlySpend}, independent of how actively you use it. By clicking "I understand", you agree to the additional costs.{' '} Compute Costs {' '} are non-refundable.

) } const PageLayout = withAuth(({ children }: PropsWithChildren) => { return {children} }) Wizard.getLayout = (page) => ( {page} ) export default Wizard